import { EventManager } from './event-bus'
import type { PDFSource, ViewerOptions, VisibleRange } from './types'
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist'
import * as pdfjsLib from 'pdfjs-dist'

// 必须在导入 PDFPage 之前设置 globalThis.pdfjsLib
// 避免 pdfjs-dist/web/pdf_viewer 中的代码在解构时找不到 pdfjsLib
if (!(globalThis as any).pdfjsLib) {
  ;(globalThis as any).pdfjsLib = pdfjsLib
}

import { PDFPage } from './page'

export class PDFViewer {
  private options: Required<ViewerOptions>
  private bus = new EventManager()
  private content: HTMLElement
  private doc: any | null = null
  private pages: PDFPage[] = []
  private baseWidth = 0
  private baseHeight = 0
  private scale: number
  private lastVisibleStart: number | null = null
  private scrollTimer: number | null = null
  private scrollDelay = 120
  private updatingVisible = false
  private pendingVisible = false
  private pendingVisibleForce = false

  /**
   * @description 构造函数，初始化容器与基础配置
   * @param options 传入的 Viewer 配置项
   * @returns void
   */
  constructor(options: ViewerOptions) {
    const {
      container,
      initialScale = 1,
      minScale = 0.5,
      maxScale = 3,
      overscan = 1,
    } = options
    this.options = { container, initialScale, minScale, maxScale, overscan }
    this.scale = initialScale

    this.content = document.createElement('div')
    this.content.style.position = 'relative'
    this.content.style.width = '100%'
    this.options.container.style.overflow = 'auto'
    this.options.container.appendChild(this.content)

    this.bindScroll()
    this.setupWorker()
  }

  /**
   * @description 加载 PDF 文档
   * @param src 文档来源（url 或二进制数据）
   * @returns Promise<void>
   */
  /**
   * @description 加载 PDF 文档
   * @param src 文档来源（url 或二进制数据）
   * @returns Promise<void>
   */
  async load(src: PDFSource): Promise<void> {
    await this.destroy()
    const params = 'url' in src ? { url: src.url } : { data: src.data }
    const loadingTask = getDocument(params)
    this.doc = await loadingTask.promise

    this.bus.emit('document:loaded', { pages: this.doc.numPages })

    await this.prepareLayout()
    this.updateVisible()
  }

  /**
   * @description 设置缩放比例
   * @param scale 缩放值
   * @returns void
   */
  /**
   * @description 设置缩放比例
   * @param scale 缩放值
   * @returns void
   */
  setScale(scale: number): void {
    const next = Math.min(
      this.options.maxScale,
      Math.max(this.options.minScale, scale)
    )
    if (next === this.scale) return
    this.scale = next
    this.bus.emit('scale:change', next)
    void this.recalculateDimensions(true)
  }

  /**
   * @description 滚动到指定页
   * @param index 页索引（1-based）
   * @returns void
   */
  /**
   * @description 滚动到指定页
   * @param index 页索引（1-based）
   * @returns void
   */
  scrollToPage(index: number): void {
    const i = index - 1
    const el = this.content.children[i]
    if (!el) return
    const top = (el as HTMLElement).offsetTop
    this.options.container.scrollTo({ top, behavior: 'smooth' })
  }

  /**
   * @description 按当前缩放重新计算页面尺寸并更新占位
   * @param triggerRender 是否在尺寸更新后触发重绘
   * @returns Promise<void>
   */
  private async recalculateDimensions(triggerRender = false): Promise<void> {
    if (!this.doc) return
    const first = await this.doc.getPage(1)
    const viewport = first.getViewport({ scale: this.scale })
    this.baseWidth = Math.floor(viewport.width)
    this.baseHeight = Math.floor(viewport.height)
    this.updateAllHeights()
    if (triggerRender) await this.updateVisible(true)
  }

  /**
   * @description 释放资源
   * @returns Promise<void>
   */
  /**
   * @description 释放资源，清理页面与文档对象
   * @returns Promise<void>
   */
  async destroy(): Promise<void> {
    for (const page of this.pages) page.destroy()
    this.pages = []
    this.content.innerHTML = ''
    if (this.doc) {
      try {
        await this.doc.destroy()
      } catch {
        /* noop */
      }
      this.doc = null
    }
  }

  /**
   * @description 获取事件总线
   * @returns EventManager
   */
  /**
   * @description 获取事件总线实例
   * @returns EventManager
   */
  getBus(): EventManager {
    return this.bus
  }

  /**
   * @description 初始化 PDF.js Worker（ESM）
   * @returns void
   */
  private setupWorker(): void {
    try {
      const worker = new Worker(
        new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url),
        { type: 'module' }
      )
      ;(GlobalWorkerOptions as any).workerPort = worker
    } catch {
      // ignore worker setup failure; pdfjs will fallback
    }
  }

  /**
   * @description 绑定容器滚动事件并派发
   * @returns void
   */
  private bindScroll(): void {
    this.options.container.addEventListener('scroll', () => {
      this.bus.emit('scroll', { top: this.options.container.scrollTop })
      if (this.scrollTimer) window.clearTimeout(this.scrollTimer)
      this.scrollTimer = window.setTimeout(() => {
        void this.updateVisible(false)
      }, this.scrollDelay)
    })
  }

  /**
   * @description 预计算首屏尺寸并创建占位容器
   * @returns Promise<void>
   */
  private async prepareLayout(): Promise<void> {
    const first = await this.doc.getPage(1)
    const viewport = first.getViewport({ scale: this.scale })
    this.baseWidth = Math.floor(viewport.width)
    this.baseHeight = Math.floor(viewport.height)

    this.content.innerHTML = ''
    this.pages = []

    for (let i = 0; i < this.doc.numPages; i++) {
      const wrapper = document.createElement('div')
      wrapper.style.width = `${this.baseWidth}px`
      wrapper.style.height = `${this.baseHeight}px`
      wrapper.style.margin = '16px auto'
      wrapper.style.backgroundColor = '#fff'
      wrapper.style.boxShadow = '0 1px 3px rgba(0,0,0,0.08)'

      this.content.appendChild(wrapper)
      const page = new PDFPage(i + 1, wrapper)
      page.init(this.scale)
      this.pages.push(page)
    }
  }

  /**
   * @description 计算当前视口可见的页区间
   * @returns 可见区间范围
   */
  private computeVisible(): VisibleRange {
    const top = this.options.container.scrollTop
    const h = this.options.container.clientHeight
    const buffer = this.options.overscan

    let start = 0
    let end = this.pages.length - 1

    for (let i = 0; i < this.pages.length; i++) {
      const el = this.content.children[i] as HTMLElement
      const elTop = el.offsetTop
      const elBottom = elTop + el.offsetHeight
      if (elBottom > top) {
        start = Math.max(0, i - buffer)
        break
      }
    }
    for (let i = start; i < this.pages.length; i++) {
      const el = this.content.children[i] as HTMLElement
      const elTop = el.offsetTop
      if (elTop > top + h) {
        end = Math.min(this.pages.length - 1, i + buffer)
        break
      }
    }
    return { start, end }
  }

  /**
   * @description 根据可见区间渲染或回收页面资源
   * @param force 是否强制重绘可见页
   * @returns Promise<void>
   */
  private async updateVisible(force = false): Promise<void> {
    if (!this.doc) return
    if (this.updatingVisible) {
      this.pendingVisible = true
      this.pendingVisibleForce = this.pendingVisibleForce || force
      return
    }
    this.updatingVisible = true
    const range = this.computeVisible()
    if (range.start !== this.lastVisibleStart) {
      this.lastVisibleStart = range.start
      this.bus.emit('page:visible-change', { index: range.start + 1 })
    }
    for (let i = 0; i < this.pages.length; i++) {
      const page = this.pages[i]
      if (!page) continue
      const isVisible = i >= range.start && i <= range.end
      if (isVisible) {
        const pageProxy = await this.doc.getPage(i + 1)
        const st = page.getState()
        if (!st.rendered || force) {
          try {
            await page.render(pageProxy, this.scale)
            this.bus.emit('page:rendered', { index: i + 1 })
          } catch (e: any) {
            if (e && e.name === 'RenderingCancelledException') {
              continue
            }
            throw e
          }
        }
      } else if (page.getState().rendered) {
        page.destroy()
      }
    }
    this.updatingVisible = false
    if (this.pendingVisible) {
      const pf = this.pendingVisibleForce
      this.pendingVisible = false
      this.pendingVisibleForce = false
      await this.updateVisible(pf)
    }
  }

  /**
   * @description 更新所有占位容器尺寸以适配缩放
   * @returns void
   */
  private updateAllHeights(): void {
    for (let i = 0; i < this.content.children.length; i++) {
      const el = this.content.children[i] as HTMLElement
      el.style.width = `${Math.floor(this.baseWidth)}px`
      el.style.height = `${Math.floor(this.baseHeight)}px`
    }
  }

  /**
   * @description 获取当前可见的第一页索引（1-based）
   * @returns 当前页索引
   */
  getCurrentVisibleIndex(): number {
    const range = this.computeVisible()
    return range.start + 1
  }

  /**
   * @description 适配容器宽度的缩放
   * @param 无
   * @returns void
   */
  fitWidth(): void {
    if (!this.baseWidth) return
    const cw = this.options.container.clientWidth
    const next = cw / this.baseWidth
    this.setScale(next)
  }

  /**
   * @description 适配整页高度（尽量在当前视口完全显示一页）
   * @param 无
   * @returns void
   */
  fitPage(): void {
    if (!this.baseHeight || !this.baseWidth) return
    const ch = this.options.container.clientHeight
    const cw = this.options.container.clientWidth
    const scaleH = ch / this.baseHeight
    const scaleW = cw / this.baseWidth
    const next = Math.min(scaleH, scaleW)
    this.setScale(next)
  }

  /**
   * @description 跳转到下一页
   * @param 无
   * @returns void
   */
  nextPage(): void {
    const idx = this.getCurrentVisibleIndex()
    this.scrollToPage(Math.min(this.pages.length, idx + 1))
  }

  /**
   * @description 跳转到上一页
   * @param 无
   * @returns void
   */
  prevPage(): void {
    const idx = this.getCurrentVisibleIndex()
    this.scrollToPage(Math.max(1, idx - 1))
  }
}
